﻿/*
 * Copyright 2014 Francois Karman
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using Common.Logging;
using Quartz.Ext;
using Quartz.WebManager.Model;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;

namespace Quartz.WebManager.Logic
{
    /// <summary>
    /// Manages all the available classes that can be used as scheduler jobs.
    /// </summary>
    public static class JobDefinitionManager
    {
        /// <summary>
        /// The logger associated with the class.
        /// </summary>
        private static readonly ILog logger = LogManager.GetLogger(typeof(JobDefinitionManager));

        /// <summary>
        /// The object used to makethe methods of this class thread-safe.
        /// </summary>
        private static readonly object lockObject = new object();

        /// <summary>
        /// The list of available job definitions.
        /// </summary>
        private static readonly ICollection<JobDefinition> cache = new LinkedList<JobDefinition>();

        /// <summary>
        /// A value indicating whether the cache is initialized.
        /// </summary>
        private static bool initialized = false;

        /// <summary>
        /// Gets the currently known and available job definitions.
        /// </summary>
        public static IEnumerable<JobDefinition> Cache
        {
            get
            {
                lock (lockObject)
                {
                    if (!initialized)
                    {
                        Refresh();
                    }

                    return cache;
                }
            }
        }

        /// <summary>
        /// Loads the cache of job definitions; if not already available.
        /// </summary>
        public static void Initialize()
        {
            lock (lockObject)
            {
                if (!initialized)
                {
                    UnsafeRefresh();
                }
            }
        }

        /// <summary>
        /// Forces the refresh of the cache.
        /// </summary>
        /// <remarks>
        /// This method can be used to load new job definitions without restarting the application.
        /// </remarks>
        public static void Refresh()
        {
            lock (lockObject)
            {
                UnsafeRefresh();
            }
        }

        /// <summary>
        /// Refreshes the cache of job definitions.
        /// </summary>
        private static void UnsafeRefresh()
        {
            cache.Clear();
            RefreshFolder(Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "bin-jobs"));
            initialized = true;
        }

        /// <summary>
        /// Loads all the job definitions available in a specific folder.
        /// </summary>
        /// <param name="folder">The path of the folder to analyze.</param>
        private static void RefreshFolder(string folder)
        {
            IEnumerable<string> files;
            try
            {
                files = Directory.GetFiles(folder);
            }
            catch (IOException exception)
            {
                logger.FatalFormat("The folder containing job definitions can't be read (folder:{0})", exception, folder);
                return;
            }

            foreach (string file in files.Where(f => f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)))
            {
                try
                {
                    Assembly assembly = Assembly.LoadFile(file);
                    foreach (Type job in assembly.GetTypes().Where(t => !t.IsAbstract && t.IsPublic && typeof(IJob).IsAssignableFrom(t)))
                    {
                        cache.Add(CreateJobDefinition(job));
                    }
                }
                catch (Exception exception)
                {
                    logger.ErrorFormat("An assembly can't be loaded or read properly ({0})", exception, file);
                }
            }
        }

        /// <summary>
        /// Creates a new job definition that encapsulates the information of the job type.
        /// </summary>
        /// <param name="job">The job type.</param>
        /// <returns>The job definition associated to <paramref name="job"/>.</returns>
        private static JobDefinition CreateJobDefinition(Type job)
        {
            JobDefinition result = new JobDefinition() { TypeName = job.AssemblyQualifiedName };

            var displayNameAttribute = job.GetCustomAttribute<DisplayNameAttribute>();
            var descriptionAttribute = job.GetCustomAttribute<DescriptionAttribute>();
            var propertyAttributes = job.GetCustomAttributes<PropertyAttribute>();
            result.DisplayName = displayNameAttribute == null ? job.Name : displayNameAttribute.DisplayName;
            result.Description = descriptionAttribute == null ? null : descriptionAttribute.Description;

            foreach (PropertyAttribute property in propertyAttributes)
            {
                result.Properties.Add(new DataPropertyInfo()
                {
                    Name = property.Name,
                    Type = property.Type,
                    DisplayName = property.DisplayName,
                    Required = property.Required,
                    Description = property.Description,
                });
            }

            return result;
        }
    }
}
